﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Json;
using System.Text;
using System.Threading.Tasks;
using Windows.Storage;

namespace Enough.Storage
{
    public enum StorageLocation
    {
        Local, Roaming, Temporary
    }

    public enum SerializerType
    {
        DataContractJson, DataContractXml
    }

    public class StorageHelper
    {

        /// <summary>
        /// Serializes and stores the given object on disk
        /// </summary>
        /// <param name="toBeSaved">the object that should be saved</param>
        /// <param name="fileName">the optional file name, default to the name of the object-type</param>
        /// <param name="folder">the optional storage location, defaults to ApplicationData.Current.LocalFolder (overrides storageLocation)</param>
        /// <param name="storageLocation">the optional storage location, defaults to StorageLocation.Local</param>
        /// <param name="serializerType">the serializer type - either JSON or XML, dafaults to DataContractJson</param>
        /// <param name="knownTypes">a list of known types. This is necessary when you want to persist a list that contains different types like List&lt;IVehicle&gt; with instances of Car and Bike. In that case you would specify <code>knownTypes: new Type[]{ typeof(Car), typeof(Bike) }</code>.</param>
        public static async Task SaveObjectAsync(Object toBeSaved, string fileName = null, StorageFolder folder = null, StorageLocation storageLocation = StorageLocation.Local, SerializerType serializerType = SerializerType.DataContractJson, IEnumerable<Type> knownTypes = null)
        {
            Type type = toBeSaved.GetType();
            fileName = getFileName(fileName, type, serializerType);
            if (folder == null)
            {
                folder = getStorageFolder(storageLocation);
            }
            MemoryStream memoryStream = new MemoryStream();
            Serializer serializer = getSerializer(serializerType, type, knownTypes);
            serializer.WriteObject(memoryStream, toBeSaved);
            StorageFile file = await folder.CreateFileAsync(fileName, CreationCollisionOption.ReplaceExisting);
            using (Stream fileStream = await file.OpenStreamForWriteAsync())
            {
                memoryStream.Seek(0, SeekOrigin.Begin);
                await memoryStream.CopyToAsync(fileStream);
                await fileStream.FlushAsync();
            }
        }

        /// <summary>
        /// Loads a previously saved object
        /// </summary>
        /// <typeparam name="T">the type of the object that should be loaded</typeparam>
        /// <param name="fileName">the optional file name, default to the name of the object-type</param>
        /// <param name="folder">the optional storage location, defaults to ApplicationData.Current.LocalFolder (overrides storageLocation)</param>
        /// <param name="storageLocation">the optional storage location, defaults to StorageLocation.Local</param>
        /// <param name="serializerType">the serializer type - either JSON or XML, dafaults to DataContractJson</param>
        /// <param name="knownTypes">a list of known types. This is necessary when you want to persist a list that contains different types like List&lt;IVehicle&gt; with instances of Car and Bike. In that case you would specify <code>knownTypes: new Type[]{ typeof(Car), typeof(Bike) }</code>.</param>
        /// <returns>the loaded object</returns>
        /// <exception cref="System.IO.IOException">when loading fails</exception>
        /// <see cref="TryLoadObjectAsync"/>
        public static async Task<T> LoadObjectAsync<T>(string fileName = null, StorageFolder folder = null, StorageLocation storageLocation = StorageLocation.Local, SerializerType serializerType = SerializerType.DataContractJson, IEnumerable<Type> knownTypes = null)
        {
            Type type = typeof(T);
            fileName = getFileName(fileName, type, serializerType);
            if (folder == null)
            {
                folder = getStorageFolder(storageLocation);
            }
            StorageFile file = await folder.GetFileAsync(fileName);
            MemoryStream memoryStream = new MemoryStream();
            using (Stream fileStream = await file.OpenStreamForReadAsync())
            {
                await fileStream.CopyToAsync(memoryStream);
            }
            memoryStream.Seek(0, SeekOrigin.Begin);
            Serializer serializer = getSerializer(serializerType, type, knownTypes); ;
            T obj = (T)serializer.ReadObject(memoryStream);
            return obj;
        }

        /// <summary>
        /// Tries to load a previously saved object
        /// </summary>
        /// <typeparam name="T">the type of the object that should be loaded</typeparam>
        /// <param name="fileName">the optional file name, default to the name of the object-type</param>
        /// <param name="folder">the optional storage location, defaults to ApplicationData.Current.LocalFolder (overrides storageLocation)</param>
        /// <param name="storageLocation">the optional storage location, defaults to StorageLocation.Local</param>
        /// <param name="serializerType">the serializer type - either JSON or XML, dafaults to DataContractJson</param>
        /// <param name="knownTypes">a list of known types. This is necessary when you want to persist a list that contains different types like List&lt;IVehicle&gt; with instances of Car and Bike. In that case you would specify <code>knownTypes: new Type[]{ typeof(Car), typeof(Bike) }</code>.</param>
        /// <returns>the loaded object or null when it couldn't be loaded</returns>
        /// <see cref="LoadObjectAsync"/>
        public async static Task<T> TryLoadObjectAsync<T>(string fileName = null, StorageFolder folder = null, StorageLocation storageLocation = StorageLocation.Local, SerializerType serializerType = SerializerType.DataContractJson, IEnumerable<Type> knownTypes = null)
        {
            try
            {
                return await LoadObjectAsync<T>(fileName, folder, storageLocation, serializerType, knownTypes);
            }
            catch (Exception)
            {
                return default(T);
            }
        }

        /// <summary>
        /// Deletes the specfied storage gfile
        /// </summary>
        /// <typeparam name="T">the type of the object whose storage file should be deleted</typeparam>
        /// <param name="fileName">the optional file name, default to the name of the object-type</param>
        /// <param name="folder">the optional storage location, defaults to ApplicationData.Current.LocalFolder (overrides storageLocation)</param>
        /// <param name="storageLocation">the optional storage location, defaults to StorageLocation.Local</param>
        /// <param name="serializerType">the serializer type - either JSON or XML, dafaults to DataContractJson</param>
        /// <returns>true when the file exists and was deleted, false if it does not exist or couldn't be deleted</returns>
        public async static Task<bool> DeleteObjectAsync<T>(string fileName = null, StorageFolder folder = null, StorageLocation storageLocation = StorageLocation.Local, SerializerType serializerType = SerializerType.DataContractJson)
        {
            Type type = typeof(T);
            fileName = getFileName(fileName, type, serializerType);
            if (folder == null)
            {
                folder = getStorageFolder(storageLocation);
            }
            try
            {
                StorageFile file = await folder.GetFileAsync(fileName);
                await file.DeleteAsync();
                return true;
            }
            catch (Exception)
            {
                return false;
            }
        }

        /// <summary>
        /// Saves the given stream
        /// </summary>
        /// <param name="stream">the stream that contains the data that should be saved</param>
        /// <param name="fileName">the mandatory fileName</param>
        /// <param name="folder">the optional storage location, defaults to ApplicationData.Current.LocalFolder (overrides storageLocation)</param>
        /// <param name="storageLocation">the optional storage location, defaults to StorageLocation.Local</param>
        public static async Task SaveStreamAsync(Stream stream, string fileName, StorageFolder folder = null, StorageLocation storageLocation = StorageLocation.Local)
        {
            if (folder == null)
            {
                folder = getStorageFolder(storageLocation);
            }
            StorageFile file = await folder.CreateFileAsync(fileName, CreationCollisionOption.ReplaceExisting);
            using (Stream fileStream = await file.OpenStreamForWriteAsync())
            {
                stream.Seek(0, SeekOrigin.Begin);
                await stream.CopyToAsync(fileStream);
                await fileStream.FlushAsync();
            }
        }

        /// <summary>
        /// Opens a previously saved stream
        /// </summary>
        /// <param name="fileName">the mandatory fileName</param>
        /// <param name="folder">the optional storage location, defaults to ApplicationData.Current.LocalFolder (overrides storageLocation)</param>
        /// <param name="storageLocation">the optional storage location, defaults to StorageLocation.Local</param>
        public static async Task<Stream> OpenStreamAsync(string fileName, StorageFolder folder = null, StorageLocation storageLocation = StorageLocation.Local)
        {
            StorageFile file = await GetStreamFileAsync(fileName, folder, storageLocation);
            return await file.OpenStreamForReadAsync();
        }

        /// <summary>
        /// Opens a previously saved stream
        /// </summary>
        /// <param name="fileName">the mandatory fileName</param>
        /// <param name="folder">the optional storage location, defaults to ApplicationData.Current.LocalFolder (overrides storageLocation)</param>
        /// <param name="storageLocation">the optional storage location, defaults to StorageLocation.Local</param>
        public static async Task<Stream> TryOpenStreamAsync(string fileName, StorageFolder folder = null, StorageLocation storageLocation = StorageLocation.Local)
        {
            try
            {
                return await OpenStreamAsync(fileName, folder, storageLocation);
            }
            catch
            {
                return null;
            }
        }

        /// <summary>
        /// Deletes the specfied storage file
        /// </summary>
        /// <param name="fileName">the mandatory file name</param>
        /// <param name="folder">the optional storage location, defaults to ApplicationData.Current.LocalFolder (overrides storageLocation)</param>
        /// <param name="storageLocation">the optional storage location, defaults to StorageLocation.Local</param>
        /// <returns>true when the file exists and was deleted, false if it does not exist or couldn't be deleted</returns>
        public async static Task<bool> DeleteStreamAsync(string fileName, StorageFolder folder = null, StorageLocation storageLocation = StorageLocation.Local)
        {
            if (folder == null)
            {
                folder = getStorageFolder(storageLocation);
            }
            try
            {
                StorageFile file = await folder.GetFileAsync(fileName);
                await file.DeleteAsync();
                return true;
            }
            catch (Exception)
            {
                return false;
            }
        }
        /// <summary>
        /// Checks if the specified stream file already exists.
        /// </summary>
        /// <param name="fileName">the name of the file</param>
        /// <param name="folder">the optional storage location, defaults to ApplicationData.Current.LocalFolder (overrides storageLocation)</param>
        /// <param name="storageLocation">the optional storage location, defaults to StorageLocation.Local</param>
        /// <returns>true when the specified file exists</returns>
        public static async Task<bool> ExistsAsync(string fileName, StorageFolder folder = null, StorageLocation storageLocation = StorageLocation.Local)
        {
            try
            {
                StorageFile file = await GetStreamFileAsync(fileName, folder, storageLocation);
                return (file != null);
            }
            catch (Exception)
            {
                return false;
            }
        }

        /// <summary>
        /// Retrieves the used StorageFile for the given location.
        /// </summary>
        /// <param name="fileName">the name of the file</param>
        /// <param name="folder">the optional storage location, defaults to ApplicationData.Current.LocalFolder (overrides storageLocation)</param>
        /// <param name="storageLocation">the optional storage location, defaults to StorageLocation.Local</param>
        /// <returns>the StorageFile that is being used</returns>
        public static async Task<StorageFile> GetStreamFileAsync(string fileName, StorageFolder folder = null, StorageLocation storageLocation = StorageLocation.Local)
        {
            if (folder == null)
            {
                folder = getStorageFolder(storageLocation);
            }
            StorageFile file = await folder.GetFileAsync(fileName);
            return file;
        }


        #region private helper methods
        private static string getFileName(string fileName, Type type, SerializerType serializerType)
        {
            if (fileName != null)
            {
                return fileName;
            }
            fileName = type.ToString().Replace('.', '_').Replace('[', '@').Replace(']', '@');
            switch (serializerType)
            {
                case SerializerType.DataContractJson:
                    fileName += ".json";
                    break;
                case SerializerType.DataContractXml:
                    fileName += ".xml";
                    break;
                default:
                    throw new ArgumentOutOfRangeException("for SerializerType " + serializerType);
            }
            return fileName;
        }

        private static StorageFolder getStorageFolder(StorageLocation location)
        {
            switch (location)
            {
                case StorageLocation.Local:
                    return ApplicationData.Current.LocalFolder;
                case StorageLocation.Roaming:
                    return ApplicationData.Current.RoamingFolder;
                case StorageLocation.Temporary:
                    return ApplicationData.Current.TemporaryFolder;
            }
            throw new ArgumentOutOfRangeException("for StorageType " + location);
        }


        private static Serializer getSerializer(SerializerType serializerType, Type type, IEnumerable<Type> knownTypes)
        {
            switch (serializerType)
            {
                case SerializerType.DataContractJson:
                    return new JsonContractSerializer(type, knownTypes);
                case SerializerType.DataContractXml:
                    return new XmlContractSerializer(type, knownTypes);
            }
            throw new ArgumentOutOfRangeException("for SerializerType " + serializerType);
        }
        #endregion

        #region serializer
        interface Serializer
        {
            object ReadObject(Stream stream);

            void WriteObject(Stream stream, object graph);
        }

        class JsonContractSerializer : Serializer
        {
            private DataContractJsonSerializer _serializer;

            public JsonContractSerializer(Type type, IEnumerable<Type> knownTypes)
            {
                this._serializer = new DataContractJsonSerializer(type, knownTypes);
            }

            public object ReadObject(Stream stream)
            {
                return _serializer.ReadObject(stream);
            }

            public void WriteObject(Stream stream, object graph)
            {
                _serializer.WriteObject(stream, graph);
            }
        }

        class XmlContractSerializer : Serializer
        {
            private DataContractSerializer _serializer;

            public XmlContractSerializer(Type type, IEnumerable<Type> knownTypes)
            {
                this._serializer = new DataContractSerializer(type, knownTypes);
            }

            public object ReadObject(Stream stream)
            {
                return _serializer.ReadObject(stream);
            }

            public void WriteObject(Stream stream, object graph)
            {
                _serializer.WriteObject(stream, graph);
            }
        }
        #endregion
    }
}
